package editor.animation;

import animation.Animation;
import animation.AnimationStateMachine;
import animation.AnimationTree;
import animation.Skeleton;
import com.crunch.math.MathOps;
import com.crunch.math.Vector2f;
import com.crunch.math.Vector4f;

import javax.swing.*;
import javax.swing.event.*;
import javax.swing.tree.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.util.Enumeration;
import java.util.HashMap;

public class AnimationPanel extends EditorPanel {
	private JPanel animationsControlsPanel;
	private JButton addAnimationButton;
	private JButton removeAnimationButton;
	private JTextField nameField;
	private JList<String> animationsList;
	private DefaultListModel<String> listModel;

	private JPanel animationControlsPanel;
	private JSpinner lengthSpinner;
	private JSpinner fadeInSpinner;
	private JSpinner fadeOutSpinner;
	private JCheckBox boneEnabledCheckbox;
	private JTree boneTree;
	private DefaultTreeModel treeModel;

	private JPanel keyframeControlsPanel;
	private JButton playButton;
	private JButton stopButton;
	private Timeline timeline;
	private JSpinner timeSpinner;
	private JSpinner translationXSpinner;
	private JSpinner translationYSpinner;
	private JSpinner rotationSpinner;
	private JSpinner scaleXSpinner;
	private JSpinner scaleYSpinner;

	private boolean performCallbacks = true;

	private static final Vector4f ACTIVE_COLOR = new Vector4f(1.0f, 1.0f, 1.0f, 1.0f);
	private static final Vector4f INACTIVE_COLOR = new Vector4f(0.5f, 0.5f, 0.5f, 1.0f);
	private static final Vector4f SELECTED_COLOR = new Vector4f(1.0f, 0.0f, 0.0f, 1.0f);
	private static final Color ACTIVE_TEXT_COLOR = Color.BLACK;
	private static final Color INACTIVE_TEXT_COLOR = Color.GRAY;

	private boolean playing;
	private float prevCurrentTime;

	@Override
	public String getTabName() {
		return "Animations";
	}

	public AnimationPanel(AnimationEditor editor) {
		this.editor = editor;
		GroupLayout topLayout = new GroupLayout(this);
		setLayout(topLayout);
		topLayout.setAutoCreateGaps(true);
		topLayout.setAutoCreateContainerGaps(true);

		// animations controls/list
		// animation controls
		// bone tree
		// bone controls

		animationsControlsPanel = new JPanel();
		animationsControlsPanel.setBorder(BorderFactory.createEtchedBorder());
		{
			GroupLayout layout = new GroupLayout(animationsControlsPanel);
			animationsControlsPanel.setLayout(layout);
			layout.setAutoCreateGaps(true);
			layout.setAutoCreateContainerGaps(true);

			addAnimationButton = new JButton("Add animation");
			addAnimationButton.addActionListener(new ActionListener() {
				@Override
				public void actionPerformed(ActionEvent e) {
					synchronized (AnimationPanel.this.editor) {
						addAnimation();
					}
				}
			});

			removeAnimationButton = new JButton("Remove animation");
			removeAnimationButton.addActionListener(new ActionListener() {
				@Override
				public void actionPerformed(ActionEvent e) {
					synchronized (AnimationPanel.this.editor) {
						removeAnimation();
					}
				}
			});

			JLabel nameLabel = new JLabel("Name:", JLabel.TRAILING);
			nameField = new JTextField();
			nameField.addActionListener(new ActionListener() {
				@Override
				public void actionPerformed(ActionEvent e) {
					synchronized (AnimationPanel.this.editor) {
						if (!performCallbacks) {
							return;
						}
						storeAnimationProperties();
					}
				}
			});
			nameField.addFocusListener(new FocusListener() {
				@Override
				public void focusGained(FocusEvent e) {
				}

				@Override
				public void focusLost(FocusEvent e) {
					synchronized (AnimationPanel.this.editor) {
						if (!performCallbacks) {
							return;
						}
						storeAnimationProperties();
					}
				}
			});
			nameLabel.setLabelFor(nameField);

			animationsList = new JList<String>(new DefaultListModel<String>());
			animationsList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
			listModel = (DefaultListModel<String>) animationsList.getModel();
			animationsList.addListSelectionListener(new ListSelectionListener() {
				@Override
				public void valueChanged(ListSelectionEvent e) {
					synchronized (AnimationPanel.this.editor) {
						if (!performCallbacks) {
							return;
						}

						int sel = -1;
						for (int i = e.getFirstIndex(); sel == -1 && i <= e.getLastIndex() && i < listModel.size(); ++i) {
							if (animationsList.isSelectedIndex(i)) {
								sel = i;
							}
						}

						storeKeyframeProperties();
						keyframeSelection = -1;
						setKeyframeControlsEnabled(false);

						storeBoneProperties();
						boneSelection = -1;
						boolean tempPerformCallbacks = performCallbacks;
						performCallbacks = false;
						try {
							boneTree.clearSelection();
						} finally {
							performCallbacks = tempPerformCallbacks;
						}
						setBoneControlsEnabled(false);

						storeAnimationProperties();
						timeline.clearKeyframes(); // clear the timeline
						animationSelection = sel;
						setAnimationControlsEnabled(sel >= 0);
						if (sel >= 0) {
							loadAnimationProperties();
						}

						constructCharacterInstance();
					}
				}
			});
			JScrollPane animationsListScroll = new JScrollPane(animationsList);
			animationsListScroll.setMinimumSize(new Dimension(animationsListScroll.getMinimumSize().width, 64));
			animationsListScroll.setMaximumSize(new Dimension(animationsListScroll.getMaximumSize().width, 128));

			layout.setHorizontalGroup(layout.createParallelGroup()
					.addGroup(layout.createSequentialGroup()
							.addComponent(addAnimationButton)
							.addComponent(removeAnimationButton)
							.addComponent(nameLabel)
							.addComponent(nameField))
					.addComponent(animationsListScroll));
			layout.setVerticalGroup(layout.createSequentialGroup()
					.addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
							.addComponent(addAnimationButton)
							.addComponent(removeAnimationButton)
							.addComponent(nameLabel)
							.addComponent(nameField))
					.addComponent(animationsListScroll));

		}

		animationControlsPanel = new JPanel();
		animationControlsPanel.setBorder(BorderFactory.createEtchedBorder());
		{
			final int MIN_TEXT_WIDTH = 60;

			GroupLayout layout = new GroupLayout(animationControlsPanel);
			animationControlsPanel.setLayout(layout);
			layout.setAutoCreateGaps(true);
			layout.setAutoCreateContainerGaps(true);

			ChangeListener changeListener = new ChangeListener() {
				@Override
				public void stateChanged(ChangeEvent e) {
					synchronized (AnimationPanel.this.editor) {
						if (!performCallbacks) {
							return;
						}
						storeAnimationProperties();
					}
				}
			};

			JLabel lengthLabel = new JLabel("Length:", JLabel.TRAILING);
			lengthSpinner = new JSpinner(new SpinnerNumberModel(new Float(0.0f), new Float(0.0f), new Float(100000.0f), new Float(1.0f)));
			lengthSpinner.setMinimumSize(new Dimension(MIN_TEXT_WIDTH, lengthSpinner.getMinimumSize().height));
			lengthSpinner.addChangeListener(changeListener);
			lengthLabel.setLabelFor(lengthSpinner);

			JLabel fadeInLabel = new JLabel("Fade in time:", JLabel.TRAILING);
			fadeInSpinner = new JSpinner(new SpinnerNumberModel(new Float(0.0f), new Float(0.0f), new Float(100000.0f), new Float(1.0f)));
			fadeInSpinner.setMinimumSize(new Dimension(MIN_TEXT_WIDTH, fadeInSpinner.getMinimumSize().height));
			fadeInSpinner.addChangeListener(changeListener);
			fadeInLabel.setLabelFor(fadeInLabel);

			JLabel fadeOutLabel = new JLabel("Fade out time:", JLabel.TRAILING);
			fadeOutSpinner = new JSpinner(new SpinnerNumberModel(new Float(0.0f), new Float(0.0f), new Float(100000.0f), new Float(1.0f)));
			fadeOutSpinner.setMinimumSize(new Dimension(MIN_TEXT_WIDTH, fadeOutSpinner.getMinimumSize().height));
			fadeOutSpinner.addChangeListener(changeListener);
			fadeOutLabel.setLabelFor(fadeOutSpinner);

			boneEnabledCheckbox = new JCheckBox("Bone enabled");
			boneEnabledCheckbox.addChangeListener(new ChangeListener() {
				@Override
				public void stateChanged(ChangeEvent e) {
					synchronized (AnimationPanel.this.editor) {
						if (!performCallbacks) {
							return;
						}
						storeBoneProperties();
					}
				}
			});

			boneTree = new JTree(new ColoredNode("skeleton"));
			boneTree.setCellRenderer(new ColoredNodeRenderer());
			treeModel = (DefaultTreeModel) boneTree.getModel();
			boneTree.setShowsRootHandles(true);
			boneTree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
			boneTree.addTreeSelectionListener(new TreeSelectionListener() {
				@Override
				public void valueChanged(TreeSelectionEvent e) {
					synchronized (AnimationPanel.this.editor) {
						if (!performCallbacks) {
							return;
						}

						TreePath path = e.getNewLeadSelectionPath();
						ColoredNode node = (path == null || path.getLastPathComponent() == treeModel.getRoot()) ?
								null : (ColoredNode) path.getLastPathComponent();
						int sel;
						if (node == null) {
							sel = -1;
						} else {
							sel = AnimationPanel.this.editor.skeleton.getBoneIndex(node.toString());
						}

						storeKeyframeProperties();
						keyframeSelection = -1;
						setKeyframeControlsEnabled(false);

						storeBoneProperties();
						boneSelection = sel;
						setBoneControlsEnabled(sel >= 0);
						if (sel >= 0) {
							loadBoneProperties();
						}

						updateBoneColors();

						// if the new bone has a keyframe at keyframeTime, automatically select it
						if (timeline.hasKeyframe(boneSelection, timeline.getCurrentTime())) { // checks for sel >= 0 automatically
							timeline.selectKeyframeAtCurrentTime(true);
						}
					}
				}
			});
			JScrollPane boneTreeScroll = new JScrollPane(boneTree);

			layout.setHorizontalGroup(layout.createParallelGroup()
					.addGroup(layout.createSequentialGroup()
							.addComponent(lengthLabel)
							.addComponent(lengthSpinner)
							.addComponent(fadeInLabel)
							.addComponent(fadeInSpinner)
							.addComponent(fadeOutLabel)
							.addComponent(fadeOutSpinner))
					.addComponent(boneEnabledCheckbox)
					.addComponent(boneTreeScroll));
			layout.setVerticalGroup(layout.createSequentialGroup()
					.addGroup(layout.createParallelGroup()
							.addComponent(lengthLabel)
							.addComponent(lengthSpinner)
							.addComponent(fadeInLabel)
							.addComponent(fadeInSpinner)
							.addComponent(fadeOutLabel)
							.addComponent(fadeOutSpinner))
					.addComponent(boneEnabledCheckbox)
					.addComponent(boneTreeScroll));
		}

		keyframeControlsPanel = new JPanel();
		keyframeControlsPanel.setBorder(BorderFactory.createEtchedBorder());
		{
			GroupLayout layout = new GroupLayout(keyframeControlsPanel);
			keyframeControlsPanel.setLayout(layout);
			layout.setAutoCreateGaps(true);
			layout.setAutoCreateContainerGaps(true);

			playButton = new JButton(new ImageIcon(this.getClass().getResource("/toolbarButtonGraphics/media/Play16.gif")));
			playButton.addActionListener(new ActionListener() {
				@Override
				public void actionPerformed(ActionEvent e) {
					synchronized (AnimationPanel.this.editor) {
						play();
					}
				}
			});
			stopButton = new JButton(new ImageIcon(this.getClass().getResource("/toolbarButtonGraphics/media/Stop16.gif")));
			stopButton.addActionListener(new ActionListener() {
				@Override
				public void actionPerformed(ActionEvent e) {
					synchronized (AnimationPanel.this.editor) {
						stop();
					}
				}
			});

			timeline = new Timeline();
			timeline.setColors(new Color(0.5f, 1.0f, 1.0f), Color.YELLOW, Color.LIGHT_GRAY, Color.RED);
			timeline.setActiveTrack(-1, false);
			timeline.setTimelineListener(new Timeline.TimelineListener() {
				@Override
				public void keyframeSelected(int track, Float prevTime, Float newTime) {
					synchronized (AnimationPanel.this.editor) {
						if (newTime == null) {
							setKeyframeControlsEnabled(false);
							keyframeSelection = -1;
						} else {
							Animation animation = AnimationPanel.this.editor.animations.get(animationSelection);
							Animation.AnimatedBone bone = animation.getBone(AnimationPanel.this.editor.skeleton.getBone(boneSelection).getName());
							keyframeSelection = bone.getLowerKeyframe(newTime);
							assert(bone.getKeyframe(keyframeSelection).getTime() == newTime);
							loadKeyframeProperties();
							setKeyframeControlsEnabled(true);
						}
					}
				}

				@Override
				public void keyframeAdded(int track, Float time) {
					synchronized (AnimationPanel.this.editor) {
						addKeyframe(time);
					}
				}

				@Override
				public void keyframeRemoved(int track, Float time) {
					synchronized (AnimationPanel.this.editor) {
						removeKeyframe(time);
					}
				}

				@Override
				public void keyframeMoved(int track, Float prevTime, Float newTime) {
					synchronized (AnimationPanel.this.editor) {
						// automatically makes call to store keyframe
						timeSpinner.setValue(newTime);
					}
				}

				@Override
				public void currentTimeChanged(float currentTime) {
					synchronized (AnimationPanel.this.editor) {
						AnimationPanel.this.editor.state.setVariable("time", currentTime);
						AnimationPanel.this.editor.state.triggerEvent("setTime");
						AnimationPanel.this.editor.evaluator.evaluate(AnimationPanel.this.editor.state);
					}
				}
			});

			final int MIN_TEXT_WIDTH = 64;

			ChangeListener changeListener = new ChangeListener() {
				@Override
				public void stateChanged(ChangeEvent e) {
					synchronized (AnimationPanel.this.editor) {
						if (!performCallbacks) {
							return;
						}
						storeKeyframeProperties();
					}
				}
			};

			JLabel timeLabel = new JLabel("Time:", JLabel.TRAILING);
			timeSpinner = new JSpinner(new SpinnerNumberModel(new Float(0.0f), new Float(-100000.0f), new Float(100000.0f), new Float(1.0f)));
			timeSpinner.setMinimumSize(new Dimension(MIN_TEXT_WIDTH, timeSpinner.getMinimumSize().height));
			timeSpinner.addChangeListener(changeListener);
			timeLabel.setLabelFor(timeSpinner);

			JLabel translationLabel = new JLabel("Translation:", JLabel.TRAILING);
			translationXSpinner = new JSpinner(new SpinnerNumberModel(new Float(0.0f), new Float(-100000.0f), new Float(100000.0f), new Float(0.1f)));
			translationXSpinner.setMinimumSize(new Dimension(MIN_TEXT_WIDTH, translationXSpinner.getMinimumSize()
					.height));
			translationXSpinner.addChangeListener(changeListener);
			translationYSpinner = new JSpinner(new SpinnerNumberModel(new Float(0.0f), new Float(-100000.0f), new Float(100000.0f), new Float(0.1f)));
			translationYSpinner.setMinimumSize(new Dimension(MIN_TEXT_WIDTH, translationYSpinner.getMinimumSize()
					.height));
			translationYSpinner.addChangeListener(changeListener);
			translationLabel.setLabelFor(translationXSpinner);

			JLabel rotationLabel = new JLabel("Rotation:", JLabel.TRAILING);
			rotationSpinner = new JSpinner(new SpinnerNumberModel(new Float(0.0f), new Float(-100000.0f), new Float(100000.0f), new Float(1.0f)));
			rotationSpinner.setMinimumSize(new Dimension(MIN_TEXT_WIDTH, rotationSpinner.getMinimumSize().height));
			rotationSpinner.addChangeListener(changeListener);
			rotationLabel.setLabelFor(rotationSpinner);

			JLabel scaleLabel = new JLabel("Scale:", JLabel.TRAILING);
			scaleXSpinner = new JSpinner(new SpinnerNumberModel(new Float(1.0f), new Float(-100000.0f), new Float(100000.0f), new Float(0.1f)));
			scaleXSpinner.setMinimumSize(new Dimension(MIN_TEXT_WIDTH, scaleXSpinner.getMinimumSize().height));
			scaleXSpinner.addChangeListener(changeListener);
			scaleYSpinner = new JSpinner(new SpinnerNumberModel(new Float(1.0f), new Float(-100000.0f), new Float(100000.0f), new Float(0.1f)));
			scaleYSpinner.setMinimumSize(new Dimension(MIN_TEXT_WIDTH, scaleYSpinner.getMinimumSize().height));
			scaleYSpinner.addChangeListener(changeListener);
			scaleLabel.setLabelFor(scaleXSpinner);

			layout.setHorizontalGroup(layout.createParallelGroup()
					.addGroup(layout.createSequentialGroup()
							.addComponent(playButton)
							.addComponent(stopButton))
					.addComponent(timeline.getComponent())
					.addGroup(layout.createSequentialGroup()
							.addGroup(layout.createParallelGroup(GroupLayout.Alignment.TRAILING)
									.addComponent(timeLabel)
									.addComponent(translationLabel))
							.addGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING)
									.addComponent(timeSpinner)
									.addGroup(layout.createSequentialGroup()
											.addComponent(translationXSpinner)
											.addComponent(translationYSpinner)))
							.addGroup(layout.createParallelGroup(GroupLayout.Alignment.TRAILING)
									.addComponent(rotationLabel)
									.addComponent(scaleLabel))
							.addGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING)
									.addComponent(rotationSpinner)
									.addGroup(layout.createSequentialGroup()
											.addComponent(scaleXSpinner)
											.addComponent(scaleYSpinner)))));
			layout.setVerticalGroup(layout.createSequentialGroup()
					.addGroup(layout.createParallelGroup()
							.addComponent(playButton)
							.addComponent(stopButton))
					.addComponent(timeline.getComponent())
					.addGroup(layout.createParallelGroup()
							.addComponent(timeLabel)
							.addComponent(timeSpinner)
							.addComponent(rotationLabel)
							.addComponent(rotationSpinner))
					.addGroup(layout.createParallelGroup()
							.addComponent(translationLabel)
							.addComponent(translationXSpinner)
							.addComponent(translationYSpinner)
							.addComponent(scaleLabel)
							.addComponent(scaleXSpinner)
							.addComponent(scaleYSpinner)));
		}

		topLayout.setHorizontalGroup(topLayout.createParallelGroup()
				.addComponent(animationsControlsPanel)
				.addComponent(animationControlsPanel)
				.addComponent(keyframeControlsPanel));
		topLayout.setVerticalGroup(topLayout.createSequentialGroup()
				.addComponent(animationsControlsPanel)
				.addComponent(animationControlsPanel)
				.addComponent(keyframeControlsPanel));
	}

	@Override
	public void select() {
		boolean tempPerformCallbacks = performCallbacks;
		performCallbacks = false;
		try {
			// clear the tree and rebuild it up
			boneTree.clearSelection();
			ColoredNode root = (ColoredNode) treeModel.getRoot();
			root.removeAllChildren();
			treeModel.reload();

			for (int i = 0; i < editor.skeleton.getRootBoneCount(); ++i) {
				addTreeNodes(editor.skeleton.getRootBone(i), root);
			}

			// clear the animations list and build it up
			animationsList.clearSelection();
			listModel.clear();

			// add animations
			for (int i = 0; i < editor.animationNames.size(); ++i) {
				listModel.addElement(editor.animationNames.get(i));
			}
		} finally {
			performCallbacks = tempPerformCallbacks;
		}

		// expand the whole tree - note, this is probably n^2 due to the getRowCount() call in the loop
		for (int i = 0; i < boneTree.getRowCount(); ++i) {
			boneTree.expandRow(i);
		}

		animationsList.setEnabled(true);

		animationSelection = -1;
		setAnimationControlsEnabled(false);
		boneSelection = -1;
		setBoneControlsEnabled(false);
		keyframeSelection = -1;
		setKeyframeControlsEnabled(false);

		playing = false;

		constructCharacterInstance();
	}

	@Override
	public void deselect() {}

	@Override
	public void close() {}

	private void addTreeNodes(int idx, ColoredNode parent) {
		Skeleton.Bone bone = editor.skeleton.getBone(idx);

		ColoredNode node = new ColoredNode(bone.getName());
		treeModel.insertNodeInto(node, parent, parent.getChildCount());

		for (int i = 0; i < bone.getChildCount(); ++i) {
			addTreeNodes(bone.getChild(i), node);
		}
	}

	private int keyframeSelection;  // ID of keyframe selected within animated bone
	private int boneSelection;      // ID of bone selected within skeleton
	private int animationSelection; // ID of animation selected within list

	private void storeKeyframeProperties() {
		if (keyframeSelection < 0) {
			return;
		}
		boolean tempPerformCallbacks = performCallbacks;
		performCallbacks = false;
		try {
			Animation animation = editor.animations.get(animationSelection);
			Animation.AnimatedBone bone = animation.getBone(editor.skeleton.getBone(boneSelection).getName());
			Animation.Keyframe keyframe = bone.getKeyframe(keyframeSelection);

			keyframe.setTranslation(new Vector2f(
					(Float) translationXSpinner.getValue(),
					(Float) translationYSpinner.getValue()));

			float rotation = (Float) rotationSpinner.getValue();
			rotation %= 360.0f;
			if (rotation < 0.0f) {
				rotation += 360.0f;
			}
			keyframe.setRotation((float) Math.toRadians(rotation));
			rotationSpinner.setValue(rotation);

			keyframe.setScale(new Vector2f(
					(Float) scaleXSpinner.getValue(),
					(Float) scaleYSpinner.getValue()));
			// change the keyframe time if necessary
			float time = (Float) timeSpinner.getValue();
			if (time != keyframe.getTime()) {
				int lowerIndex = bone.getLowerKeyframe(time);
				if (lowerIndex >= 0 && bone.getKeyframe(lowerIndex).getTime() == time) {
					// can't move keyframe - revert back
					timeSpinner.setValue(keyframe.getTime());
				} else {
					// remove and add at correct time (index changes)
					float oldTime = keyframe.getTime();
					bone.removeKeyframe(keyframeSelection);
					keyframeSelection = bone.addKeyframe(time);
					Animation.Keyframe movedKeyframe = bone.getKeyframe(keyframeSelection);
					movedKeyframe.setTranslation(keyframe.getTranslation());
					movedKeyframe.setRotation(keyframe.getRotation());
					movedKeyframe.setScale(keyframe.getScale());

					if (timeline.hasKeyframe(boneSelection, oldTime)) {
						// move this keyframe to the new time
						timeline.removeKeyframe(boneSelection, oldTime, false);
						timeline.addKeyframe(boneSelection, time, false);
					} // otherwise, the timeline triggered this, so the keyframe was already moved
				}
			}
		} finally {
			performCallbacks = tempPerformCallbacks;
		}

		constructCharacterInstance();
	}

	private void loadKeyframeProperties() {
		boolean tempPerformCallbacks = performCallbacks;
		performCallbacks = false;
		try {
			Animation animation = editor.animations.get(animationSelection);
			Animation.AnimatedBone bone = animation.getBone(editor.skeleton.getBone(boneSelection).getName());
			Animation.Keyframe keyframe = bone.getKeyframe(keyframeSelection);

			timeSpinner.setValue(keyframe.getTime());
			translationXSpinner.setValue(keyframe.getTranslation().x());
			translationYSpinner.setValue(keyframe.getTranslation().y());
			rotationSpinner.setValue((float) Math.toDegrees(keyframe.getRotation()));
			scaleXSpinner.setValue(keyframe.getScale().x());
			scaleYSpinner.setValue(keyframe.getScale().y());
		} finally {
			performCallbacks = tempPerformCallbacks;
		}
	}

	private void setKeyframeControlsEnabled(boolean e) {
		boolean tempPerformCallbacks = performCallbacks;
		performCallbacks = false;
		try {
			timeSpinner.setEnabled(e);
			translationXSpinner.setEnabled(e);
			translationYSpinner.setEnabled(e);
			rotationSpinner.setEnabled(e);
			scaleXSpinner.setEnabled(e);
			scaleYSpinner.setEnabled(e);
		} finally {
			performCallbacks = tempPerformCallbacks;
		}
	}

	private void storeBoneProperties() {
		if (boneSelection < 0) {
			return;
		}
		boolean tempPerformCallbacks = performCallbacks;
		performCallbacks = false;
		try {
			Animation animation = editor.animations.get(animationSelection);
			Animation.AnimatedBone bone = animation.getBone(editor.skeleton.getBone(boneSelection).getName());

			boolean animated = boneEnabledCheckbox.isSelected();
			if (animated && bone == null) {
				// add the bone
				animation.addAnimatedBone(editor.skeleton.getBone(boneSelection).getName());
			} if (!animated && bone != null) {
				// remove keyframes
				for (int i = 0; i < bone.getKeyframeCount(); ++i) {
					timeline.removeKeyframe(boneSelection, bone.getKeyframe(i).getTime(), false);
				}
				// remove the bone
				animation.removeAnimatedBone(editor.skeleton.getBone(boneSelection).getName());
			}
			timeline.setActiveTrack(animated ? boneSelection : -1, false);
			setBoneTreeTextColors();
		} finally {
			performCallbacks = tempPerformCallbacks;
		}

		constructCharacterInstance();
	}

	private void loadBoneProperties() {
		boolean tempPerformCallbacks = performCallbacks;
		performCallbacks = false;
		try {
			Animation animation = editor.animations.get(animationSelection);
			Animation.AnimatedBone bone = animation.getBone(editor.skeleton.getBone(boneSelection).getName());

			timeline.setActiveTrack(bone != null ? boneSelection : -1, false);
			boneEnabledCheckbox.setSelected(bone != null);
		} finally {
			performCallbacks = tempPerformCallbacks;
		}
	}

	private void setBoneControlsEnabled(boolean e) {
		boolean tempPerformCallbacks = performCallbacks;
		performCallbacks = false;
		try {
			boneEnabledCheckbox.setEnabled(e);
			timeline.setActiveTrack(e ? boneSelection : -1, false);
		} finally {
			performCallbacks = tempPerformCallbacks;
		}
	}

	private void storeAnimationProperties() {
		if (animationSelection < 0) {
			return;
		}
		boolean tempPerformCallbacks = performCallbacks;
		performCallbacks = false;
		try {
			Animation animation = editor.animations.get(animationSelection);
			String name = nameField.getText();
			if (name.isEmpty() || editor.animationNames.contains(name)) {
				// revert to current animation name
				nameField.setText(editor.animationNames.get(animationSelection));
			} else {
				// set animation's name
				editor.animationNames.set(animationSelection, name);
				listModel.set(animationSelection, name);
			}
			animation.setLength((Float) lengthSpinner.getValue());
			animation.setFadeInTime((Float) fadeInSpinner.getValue());
			animation.setFadeOutTime((Float) fadeOutSpinner.getValue());

			timeline.setActiveRange(0.0f, animation.getLength());
		} finally {
			performCallbacks = tempPerformCallbacks;
		}

		constructCharacterInstance();
	}

	private void loadAnimationProperties() {
		boolean tempPerformCallbacks = performCallbacks;
		performCallbacks = false;
		try {
			Animation animation = editor.animations.get(animationSelection);
			nameField.setText(editor.animationNames.get(animationSelection));
			lengthSpinner.setValue(animation.getLength());
			fadeInSpinner.setValue(animation.getFadeInTime());
			fadeOutSpinner.setValue(animation.getFadeOutTime());
			setBoneTreeTextColors();

			timeline.setActiveRange(0.0f, animation.getLength());
			// fill timeline with keyframes for each bone
			for (int i = 0; i < animation.getAnimatedBoneCount(); ++i) {
				Animation.AnimatedBone ab = animation.getAnimatedBone(i);
				int track = editor.skeleton.getBoneIndex(ab.getBoneName());
				for (int k = 0; k < ab.getKeyframeCount(); ++k) {
					Animation.Keyframe kf = ab.getKeyframe(k);
					timeline.addKeyframe(track, kf.getTime(), false);
				}
			}
		} finally {
			performCallbacks = tempPerformCallbacks;
		}
	}

	private void setAnimationControlsEnabled(boolean e) {
		boolean tempPerformCallbacks = performCallbacks;
		performCallbacks = false;
		try {
			nameField.setEnabled(e);
			removeAnimationButton.setEnabled(e);
			lengthSpinner.setEnabled(e);
			fadeInSpinner.setEnabled(e);
			fadeOutSpinner.setEnabled(e);
			boneTree.setEnabled(e);
			timeline.setEnabled(e);
			playButton.setEnabled(e);
			stopButton.setEnabled(false);
		} finally {
			performCallbacks = tempPerformCallbacks;
		}
	}

	private void setBoneTreeTextColors() {
		ColoredNode root = (ColoredNode) treeModel.getRoot();
		@SuppressWarnings("unchecked")
		Enumeration<ColoredNode> enumeration = root.preorderEnumeration();
		enumeration.nextElement(); // skip root

		Animation animation = editor.animations.get(animationSelection);
		while (enumeration.hasMoreElements()) {
			ColoredNode node = enumeration.nextElement();
			String name = node.toString();
			if (animation.getBone(name) != null) {
				node.setColor(ACTIVE_TEXT_COLOR);
			} else {
				node.setColor(INACTIVE_TEXT_COLOR);
			}
		}
		boneTree.repaint();
	}

	private void addKeyframe(float time) {
		Animation animation = editor.animations.get(animationSelection);
		Animation.AnimatedBone bone = animation.getBone(editor.skeleton.getBone(boneSelection).getName());

		int newIndex = bone.addKeyframe(time);
		// if the new keyframe was inserted before the current selection, increment the selection
		if (keyframeSelection >= 0 && newIndex <= keyframeSelection) {
			++keyframeSelection;
		}

		// set keyframe values
		Animation.Keyframe newKeyframe = bone.getKeyframe(newIndex);
		if (bone.getKeyframeCount() == 1) {
			// take values from skeleton
			Skeleton.Bone skelBone = editor.skeleton.getBone(boneSelection);
			newKeyframe.setTranslation(skelBone.getTranslation());
			newKeyframe.setRotation(skelBone.getRotation());
			newKeyframe.setScale(skelBone.getScale());
		} else if (newIndex == 0 || newIndex+1 == bone.getKeyframeCount()) {
			// new keyframe is first or last - take values of previous or next
			int otherIndex = (newIndex == 0) ? 1 : bone.getKeyframeCount()-2;
			Animation.Keyframe other = bone.getKeyframe(otherIndex);
			newKeyframe.setTranslation(other.getTranslation());
			newKeyframe.setRotation(other.getRotation());
			newKeyframe.setScale(other.getScale());
		} else {
			// interpolate values of surrounding keyframes
			Animation.Keyframe kf0 = bone.getKeyframe(newIndex-1);
			Animation.Keyframe kf1 = bone.getKeyframe(newIndex+1);
			float ratio = (time - kf0.getTime()) / (kf1.getTime() - kf0.getTime());
			newKeyframe.setTranslation(MathOps.lerp(kf0.getTranslation(), kf1.getTranslation(), ratio));
			newKeyframe.setRotation(MathOps.lerpAngles(kf0.getRotation(), kf1.getRotation(), ratio));
			newKeyframe.setScale(MathOps.lerp(kf0.getScale(), kf1.getScale(), ratio));
		}

		// no need to select - the timeline will automatically select this keyframe in a moment

		constructCharacterInstance();
	}

	private void removeKeyframe(float time) {
		Animation animation = editor.animations.get(animationSelection);
		Animation.AnimatedBone bone = animation.getBone(editor.skeleton.getBone(boneSelection).getName());
		int index = bone.getLowerKeyframe(time);

		// timeline will have already deselected the keyframe if it was previously selected
		assert(keyframeSelection != index);
		// shift selection index down if the one being removed is of lower value
		if (keyframeSelection >= 0 && keyframeSelection > index) {
			--keyframeSelection;
		}

		assert(bone.getKeyframe(index).getTime() == time);
		bone.removeKeyframe(index);
		// evaluate animation to display changes
		editor.evaluator.evaluate(AnimationPanel.this.editor.state);
	}

	private void addAnimation() {
		int val = 0;
		// find unused animation name
		boolean used = true;
		while (used) {
			if (!listModel.contains("anim" + val)) {
				used = false;
			} else {
				++val;
			}
		}

		String name = "anim" + val;

		// create an animation
		editor.animationNames.add(name);
		Animation animation = new Animation();
		editor.animations.add(animation);

		// add and select the animation
		listModel.addElement(name);
		constructCharacterInstance();
		animationsList.setSelectedIndex(listModel.getSize()-1);
	}

	private void removeAnimation() {
		int sel = animationSelection;

		// deselect and remove the animation
		animationsList.clearSelection();

		editor.animationNames.remove(sel);
		editor.animations.remove(sel);
		listModel.remove(sel);

		constructCharacterInstance();
	}

	public void constructCharacterInstance() {
		HashMap<String, Animation> animMap = new HashMap<String, Animation>();
		AnimationTree tree = new AnimationTree();
		AnimationStateMachine stateMachine = new AnimationStateMachine();
		if (animationSelection >= 0) {
			animMap.put(editor.animationNames.get(animationSelection),
					editor.animations.get(animationSelection));
			// construct animation tree consisting of this animation only
			AnimationTree.AnimationNode rootNode = new AnimationTree.AnimationNode("root");
			rootNode.setAnimation(editor.animationNames.get(animationSelection));
			tree.setRoot(rootNode);

			AnimationStateMachine.CommandSet cs = stateMachine.addEvent("setTime");
			AnimationStateMachine.AnimationSetTimeCommand cmd = new AnimationStateMachine.AnimationSetTimeCommand();
			cmd.setNodeName("root");
			cmd.setTime(new AnimationStateMachine.FloatBinding("time"));
			cs.addCommand(cs.getCommandCount(), cmd);
		}
		editor.character = new animation.Character(
				editor.skeleton,
				tree,
				stateMachine,
				animMap);
		editor.state = editor.character.createCharacterState();
		if (animationSelection >= 0) {
			editor.state.setVariable("time", timeline.getCurrentTime());
			editor.state.triggerEvent("setTime");
		}
		editor.evaluator.evaluate(editor.state);
		editor.boneColors = new Vector4f[editor.skeleton.getBoneCount()];
		updateBoneColors();
	}

	private void updateBoneColors() {
		for (int i = 0; i < editor.boneColors.length; ++i) {
			editor.boneColors[i] = INACTIVE_COLOR;
		}
		if (animationSelection >= 0) {
			Animation animation = editor.animations.get(animationSelection);
			for (int i = 0; i < animation.getAnimatedBoneCount(); ++i) {
				String bone = animation.getAnimatedBone(i).getBoneName();
				int boneIndex = editor.skeleton.getBoneIndex(bone);
				// check for invalid bones
				if (boneIndex >= 0) {
					editor.boneColors[boneIndex] = ACTIVE_COLOR;
				}
			}
		}
		if (boneSelection >= 0) {
			editor.boneColors[boneSelection] = SELECTED_COLOR;
			editor.state.setVariable("time", timeline.getCurrentTime());
			editor.state.triggerEvent("setTime");
		}
	}

	private void play() {
		Animation animation = editor.animations.get(animationSelection);
		if (animation.getLength() == 0.0f) {
			return;
		}

		setAnimationControlsEnabled(false);
		setBoneControlsEnabled(false);
		setKeyframeControlsEnabled(false);

		animationsList.setEnabled(false);
		playButton.setEnabled(false);
		stopButton.setEnabled(true);

		playing = true;
		prevCurrentTime = timeline.getCurrentTime();
		timeline.setCurrentTime(0.0f, true);
	}

	private void stop() {
		setAnimationControlsEnabled(true);
		if (boneSelection >= 0) {
			setBoneControlsEnabled(true);
		}
		if (keyframeSelection >= 0) {
			setKeyframeControlsEnabled(true);
		}

		playing = false;
		animationsList.setEnabled(true);
		playButton.setEnabled(true);
		stopButton.setEnabled(false);

		timeline.setCurrentTime(prevCurrentTime, true);
	}

	private final AnimationEditor editor;

	// PREVIEW FUNCTIONS

	@Override
	public void selectBoneInPreview(int boneIndex) {
		synchronized (editor) {
			if (playing || animationSelection < 0) {
				return;
			}
			if (boneIndex < 0) {
				// deselect
				boneTree.clearSelection();
			} else {
				// make sure the bone still exists
				if (boneIndex >= editor.skeleton.getBoneCount())
					return;

				String boneName = editor.skeleton.getBone(boneIndex).getName();
				// find the bone in the list
				@SuppressWarnings("unchecked")
				Enumeration<DefaultMutableTreeNode> enumeration =
						((DefaultMutableTreeNode) treeModel.getRoot()).preorderEnumeration();
				enumeration.nextElement(); // skip root
				while (enumeration.hasMoreElements()) {
					DefaultMutableTreeNode node = enumeration.nextElement();
					if (node.toString().equals(boneName)) {
						boneTree.setSelectionPath(new TreePath(node.getPath()));
						break;
					}
				}
			}
		}
	}

	@Override
	public void translateInPreview(Vector2f translation) {
		synchronized (editor) {
			if (playing || boneSelection < 0 || keyframeSelection < 0) {
				return;
			}
			translationXSpinner.setValue(translation.x());
			translationYSpinner.setValue(translation.y());
		}
	}

	@Override
	public void finishTranslateInPreview() {
	}

	@Override
	public void rotateInPreview(float rotation) {
		synchronized (editor) {
			if (playing || boneSelection < 0 || keyframeSelection < 0) {
				return;
			}
			rotationSpinner.setValue((float) Math.toDegrees(rotation));
		}
	}

	@Override
	public void finishRotateInPreview() {
	}

	@Override
	public void scaleInPreview(Vector2f scale) {
		synchronized (editor) {
			if (playing || boneSelection < 0 || keyframeSelection < 0) {
				return;
			}
			scaleXSpinner.setValue(scale.x());
			scaleYSpinner.setValue(scale.y());
		}
	}

	@Override
	public void finishScaleInPreview() {
	}

	@Override
	public Vector2f getBoneTranslation(int boneIndex) {
		synchronized (editor) {
			if (playing || !timeline.hasKeyframe(boneIndex, timeline.getCurrentTime())) { // takes care of checking boneIndex
				return null;
			}
			String boneName = editor.skeleton.getBone(boneIndex).getName();
			Animation.AnimatedBone ab = editor.animations.get(animationSelection).getBone(boneName);
			return ab.getKeyframe(ab.getLowerKeyframe(timeline.getCurrentTime())).getTranslation();
		}
	}

	@Override
	public float getBoneRotation(int boneIndex) {
		synchronized (editor) {
			if (playing || !timeline.hasKeyframe(boneIndex, timeline.getCurrentTime())) { // takes care of checking boneIndex
				return 0.0f;
			}
			String boneName = editor.skeleton.getBone(boneIndex).getName();
			Animation.AnimatedBone ab = editor.animations.get(animationSelection).getBone(boneName);
			return ab.getKeyframe(ab.getLowerKeyframe(timeline.getCurrentTime())).getRotation();
		}
	}

	@Override
	public Vector2f getBoneScale(int boneIndex) {
		synchronized (editor) {
			if (playing || !timeline.hasKeyframe(boneIndex, timeline.getCurrentTime())) { // takes care of checking boneIndex
				return null;
			}
			String boneName = editor.skeleton.getBone(boneIndex).getName();
			Animation.AnimatedBone ab = editor.animations.get(animationSelection).getBone(boneName);
			return ab.getKeyframe(ab.getLowerKeyframe(timeline.getCurrentTime())).getScale();
		}
	}

	@Override
	public void enableBoneInAnimationInPreview() {
		synchronized (editor) {
			if (playing || boneSelection < 0) {
				return;
			}
			boneEnabledCheckbox.setSelected(true);
		}
	}

	@Override
	public void addKeyframeInPreview() {
		synchronized (editor) {
			if (playing || boneSelection < 0) {
				return;
			}
			Animation animation = editor.animations.get(animationSelection);
			Animation.AnimatedBone ab = animation.getBone(editor.skeleton.getBone(boneSelection).getName());
			if (ab == null) {
				return;
			}
			if (!timeline.hasKeyframe(boneSelection, timeline.getCurrentTime())) {
				timeline.addKeyframe(boneSelection, timeline.getCurrentTime(), true);
				timeline.selectKeyframeAtCurrentTime(true);
			}
		}
	}

	@Override
	public void prevKeyframeInPreview(boolean first) {
		synchronized (editor) {
			if (playing || boneSelection < 0) {
				return;
			}
			if (first) {
				timeline.selectFirstKeyframe(true);
			} else {
				timeline.selectPrevKeyframe(true);
			}
		}
	}

	@Override
	public void nextKeyframeInPreview(boolean last) {
		synchronized (editor) {
			if (playing || boneSelection < 0) {
				return;
			}
			if (last) {
				timeline.selectLastKeyframe(true);
			} else {
				timeline.selectNextKeyframe(true);
			}
		}
	}

	@Override
	public void frameBeginInPreview() {
		synchronized (editor) {
			if (playing) {
				final float dt = 1.0f / 60.0f;
				float time = timeline.getCurrentTime();
				time += dt;
				time %= editor.animations.get(animationSelection).getLength();
				timeline.setCurrentTime(time, true);
			}
		}
	}
}
